Design accessibility feedback - #141
Merged
Merged
Conversation
MikeMcQuaid
approved these changes
Sep 2, 2026
graeme
force-pushed
the
design-accessibility-feedback
branch
from
September 2, 2026 10:38
e34a959 to
a19f19f
Compare
graeme
marked this pull request as ready for review
September 2, 2026 11:58
Colour sets can carry a High Contrast value per appearance, which macOS uses
when Accessibility → Display → Increase contrast is on. That splits two things
the palette could not previously satisfy at once: the Homebrew identity, and
WCAG AA.
The standard palette is unchanged — byte-identical to before for all 31
existing colour sets. Each token that needs it gains a high-contrast value:
standard high contrast
text.secondary #5C5C58 / #B0B0B0 #4A4A46 / #C7C7C7
text.tertiary #9C9C96 / #6B6B6B #64645E / #A9A9A9
text.link #5B9BD5 / #7AB3E0 #2D71AF / -
text.brand #E8971C / #E8971C #9A6310 / -
text.magenta #CB30E0 / #DB34F2 #BA1FCF / #E25AF4
text.cyan #00C0E8 / #3CD3FE #007A93 / -
status.success #3CB371 / #52C98A #2A7D4F / -
status.warning #F0AD4E / #F5C26B #9D610D / -
status.error #D9534F / #E87370 #CB302C / #ED908E
status.info #5B9BD5 / #7AB3E0 #2D71AF / -
Five tokens are new, each carrying the value it takes over from so nothing
moves: text.brand is the brand amber that foreground call sites read out of
brand.primary; status.warningBold is the current status.warning, kept vivid for
icons and fills; text.onWarning is the upgrades badge knockout; text.magenta
and text.cyan are the system purple and cyan the ANSI map fell through to.
AppKit's rule for relaxing a *missing* high-contrast variant is not observable
unless the system setting is on — a runtime probe just returns standard values
and looks like confirmation. So every colour set that varies by luminosity
states its high-contrast dark value outright rather than relying on fallback.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgwyUSxEn786mZUfZx8bCv
Asserts the palette by calculation rather than by eye: WCAG 2.1 relative luminance and contrast ratio over the colour sets as they ship, resolved for all four appearances. AA is required in the high-contrast appearances only. The standard palette is instead held to a monotonicity invariant — high contrast may never render a pairing worse than standard — which is what stops the accessible palette drifting behind the default one. Pairings are hand-maintained rather than a cross-product; requiring success green to stay legible on the error tint would cost chroma for a combination no screen renders. The catalogue is parsed from source rather than resolved through NSColor: SwiftPM copies .xcassets into the test bundle uncompiled, so a runtime lookup returns nil under `swift test`. Resolution is exact-match-or-universal, with a test enforcing that no colour set can declare a dark value without a high-contrast dark one. WarningGlyphTests pins an SF Symbols detail Apple owns rather than we do — that palette layer 1 is the inner mark — by rendering offscreen and sampling pixels. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EgwyUSxEn786mZUfZx8bCv
Both colours were doing two jobs at once, which is why neither could be tuned without breaking the other. Brand amber was only ever used as a foreground — the sidebar's selected item, the console status dot, badge labels — while brand.primary is defined as a fill. Those call sites move to text.brand, leaving brand.primary free to stay the undiluted Homebrew amber for filled surfaces. Warning splits the same way: status.warning stays the text colour and darkens under high contrast, status.warningBold stays vivid for glyphs, dots and fills. Doctor needed both, so severity styling grows a glyph alongside its foreground. brewWarningGlyphStyle() renders those glyphs monochrome normally and knocks the inner mark out in near-black under high contrast, where the enclosure alone is 1.8:1 on a light surface. The console maps ANSI magenta and cyan to tokens instead of SwiftUI's .purple and .cyan, which drop under 2:1 on white — output renders on an app surface, not the terminal surface. The upgrades badge keeps its white count on yellow and only goes black under high contrast, so it no longer needs to branch on colour scheme. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EgwyUSxEn786mZUfZx8bCv
The chip was a pale tint behind an accent glyph, which barely separated from the row and disappeared entirely once the row was selected and the background became the brand tint. It is now an outline: the accent draws both the glyph and a 1pt ring, with the row showing through. As a non-text graphic the bar is 3:1, which it clears on every row background including a selected one; the pale tint never did. PackageKindIconBackgroundToken goes away with the fill it named. The chip is also centred against the row rather than pinned to the first line, and hidden while a placeholder is on screen — SwiftUI redacts text and images but leaves Shape alone, so the ring stayed crisp and coloured around redacted content. Package views pick up the foreground roles from the previous commit at the same time: text.brand for amber, status.warningBold for the upgrade-available glyph and the dependents dots, and black on yellow for the uninstall-blocked badge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EgwyUSxEn786mZUfZx8bCv
The command text sat light-on-dark on the terminal surface, which put it under a system selection highlight drawn for dark-on-light: selecting a command measured 1.01:1. SwiftUI offers no hook to restyle that highlight, so the surface changes instead. The code well now uses the sidebar's surface, with the header and footer recessed around it — so the card reads as chrome wrapped around an inset well, and the well matches a surface the eye already knows from the sidebar. Command text measures 16.7:1 in light and 13.9:1 in dark. The footer keeps the raised contrast it needs as the line explaining what the command does, moving from tertiary to secondary text along with the step numbers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EgwyUSxEn786mZUfZx8bCv
Captures what the diff cannot: why there are two palettes and which one is held to AA, why the audit parses the catalogue instead of resolving NSColor, why the warning yellow is deliberately below the non-text guideline, and the probe that looks like it confirms high-contrast fallback but confirms nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EgwyUSxEn786mZUfZx8bCv
Installed rows marked an available upgrade with a bare warning glyph sitting between the package name and its kind pill. That put a status indicator ahead of the label it qualifies, and gave the same information a different shape from Discover's INSTALLED pill. Outdated is now a pill of its own, outlined like the kind pill beside it and placed after it. The green installed check moves to the same slot so both states read from the same position. Neutral grey rather than warning-coloured: an available upgrade is routine, and the version line underneath already carries the amber. Applied to the list row and the detail hero, which shared the glyph. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EgwyUSxEn786mZUfZx8bCv
graeme
force-pushed
the
design-accessibility-feedback
branch
from
September 2, 2026 22:00
a19f19f to
7e2c9a9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR: Accessible colour palette via a high-contrast variant
Summary
Feedback reported low text contrast in at least eight colour combinations. Rather than darken the default palette and lose the Homebrew identity, this adds High Contrast variants to the colour sets, which macOS swaps in when Accessibility > Display > Increase contrast is on. Conformance and brand identity no longer have to be the same colours.
Changes
Palette
text.brand(amber as a foreground, sincebrand.primaryis a fill),status.warningBold(vivid yellow for glyphs and fills, leavingstatus.warningas the text colour),text.onWarning,text.magenta,text.cyan.Views
.purpleand.cyan, which fall under 2:1 on the app surface the console actually renders on.Tests
BrewColorTokenContrastTestsasserts WCAG AA in the high-contrast appearances, plus an invariant that high contrast can never render a pairing worse than standard.WarningGlyphTestspins the SF Symbols palette layer order by rendering offscreen and sampling pixels.Why this split
The palette lands before the views consuming it, so every commit builds and tests independently.
Testing
scripts/testpasses at every commit, not just the tip--strictand BrewUILint cleanxcodebuild -scheme Brewsucceedsassetutilconfirms all 36 colour sets carry accessibility variantsPR checklist
Claude Code derived every colour value by calculation against WCAG 2.1 rather than by eye, and wrote the audit enforcing them, without ever launching the app. Visual review and every trade-off decision were mine.
Follow-ups
NSApp.appearanceif the system setting proves insufficient.